﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using B303Langf.Parser.AST;

namespace B303Langf.Parser.ContextualAnalysis
{
   public class Checker : IVisitor
   {
      private readonly IdentificationTable m_IdTable;

      public Checker()
      {
         m_IdTable = new IdentificationTable();
      }

      public void Check( Program program )
      {
         program.Visit( this, null );
      }

      public object VisitArrayIndexer( ArrayIndexer arrayIndexer, object args )
      {
         return arrayIndexer.IndexExpression.Visit( this, null );
      }

      public object VisitArrayVariablePrimaryExpression( ArrayVariablePrimaryExpression arrayVariablePrimaryExpression, object args )
      {
         arrayVariablePrimaryExpression.ArrayIndexer.Visit( this, null );
         string id = (string) arrayVariablePrimaryExpression.Identifier.Visit( this, null );

         var decl = m_IdTable.Retrieve( id );

         if( decl == null )
         {
            throw new ContextualAnalysisException( "Usage of undeclared variable: " + id );
         }
         else if( decl is VariableDeclaration )
         {
            // decorate expression with declaration and important information about the expression
            arrayVariablePrimaryExpression.Declaration = (VariableDeclaration) decl;
            if( !arrayVariablePrimaryExpression.Declaration.Type.IsArray )
            {
               throw new ContextualAnalysisException( "Trying to use an identifier that is not an array, as an array: " + id );
            }
            arrayVariablePrimaryExpression.Info = new ExpressionInfo( ExpressionType.Variable, decl.Type.WithoutArray() );
         }
         else
         {
            throw new ContextualAnalysisException( "Usage of invalid identifier: " + id );
         }

         return null;
      }

      public object VisitBinaryExpression( BinaryExpression binaryExpression, object args )
      {
         // visit the two expressions for decoration
         binaryExpression.LeftPrimary.Visit( this, null );
         binaryExpression.RightPrimary.Visit( this, null );

         // get the expression type information
         var lType = (ExpressionInfo) binaryExpression.LeftPrimary.Info;
         var rType = (ExpressionInfo) binaryExpression.RightPrimary.Info;
         var op = binaryExpression.Operator;

         // make sure left-side is variable
         if( op is AssignOperator && lType.ExpressionType == ExpressionType.Value )
         {
            throw new ContextualAnalysisException( "Left-hand side of assignment must be a variable" );
         }
         
         // make sure both sides has same type
         if( !lType.TypeDenoter.Equals( rType.TypeDenoter ) )
         {
            throw new ContextualAnalysisException( "Type mismatched" );
         }

         // make sure the aggreate returned expression has the right type
         if( lType.TypeDenoter is BoolTypeDenoter )
         {
            if( op is AssignOperator
               || op is ConditionalOrOperator
               || op is ConditionalAndOperator
               || op is EqualsOperator
               || op is NotEqualsOperator )
            {
               binaryExpression.Info = lType;
            }
            else
            {
               throw new ContextualAnalysisException( "Invalid operators applied to pool" );
            }
         }
         else if( lType.TypeDenoter is CharTypeDenoter )
         {
            if( op is AssignOperator )
            {
               binaryExpression.Info = lType;
            }
            else if( op is EqualsOperator || op is NotEqualsOperator )
            {
               binaryExpression.Info = new ExpressionInfo( ExpressionType.Value, new BoolTypeDenoter() );
            }
            else
            {
               throw new ContextualAnalysisException( "Invalid operators applied to symbol" );
            }
         }
         else // if( lType.TypeDenoter is IntegerTypeDenoter )
         {
            if( op is AssignOperator
               || op is PlusOperator
               || op is MinusOperator
               || op is MultiplyOperator
               || op is DivisionOperator )
            {
               binaryExpression.Info = lType;
            }
            else if( op is EqualsOperator
               || op is NotEqualsOperator
               || op is LessThanOperator
               || op is LessThanOrEqualsOperator
               || op is GreaterThanOperator
               || op is GreaterThanOrEqualsOperator )
            {
               binaryExpression.Info = new ExpressionInfo( ExpressionType.Value, new BoolTypeDenoter() );
            }
            else
            {
               throw new ContextualAnalysisException( "Invalid operators applied to quantity" );
            }
         }
         return null;
      }

      public object VisitVariableDeclarationWithDefinition( VariableDeclarationWithDefinition variableDeclarationWithDefinition, object args )
      {
         string id = (string) variableDeclarationWithDefinition.Identifier.Visit( this, null );
         m_IdTable.Enter( id, variableDeclarationWithDefinition );

         // if array declaration, make sure it is a static array
         if( variableDeclarationWithDefinition.Type.IsArray )
         {
            var indexer = variableDeclarationWithDefinition.Type.ArrayIndexer;
            if( indexer.HasIndex && !( indexer.IndexExpression is TypeLiteralPrimaryExpression ) )
            {
               throw new ContextualAnalysisException( "Cannot use expression to declare an array: " + id );
            }
         }

         // make sure correct types are used
         variableDeclarationWithDefinition.DefinitionExpression.Visit( this, null );
         var definitionExpressionInfo = (ExpressionInfo) variableDeclarationWithDefinition.DefinitionExpression.Info;
         variableDeclarationWithDefinition.Type.Visit( this, null );
         var lType = variableDeclarationWithDefinition.Type;
         var rType = definitionExpressionInfo.TypeDenoter;
         if( !rType.Equals( lType ) )
         {
            throw new ContextualAnalysisException( "Type mismatched" );
         }

         return null;
      }

      public object VisitBlock( Block block, object args )
      {
         foreach( var statement in block.Statements )
         {
            statement.Visit( this, args );
         }

         return null;
      }

      public object VisitBoolTypeDenoter( BoolTypeDenoter boolTypeDenoter, object args )
      {
         return null;
      }

      public object VisitBoolTypeLiteral( BoolTypeLiteral boolTypeLiteral, object args )
      {
         return BoolTypeDenoter.Normal;
      }

      public object VisitCharArrayTypeLiteral( CharArrayTypeLiteral charArrayTypeLiteral, object args )
      {
         return new CharTypeDenoter( new ArrayIndexer( new TypeLiteralPrimaryExpression( new IntegerTypeLiteral( ( charArrayTypeLiteral.Spelling.Length + 1 ).ToString() ) ) ) );
      }

      public object VisitDeclarations( Declarations declarations, object args )
      {
         foreach( var decl in declarations.List )
         {
            // make sure no function declarations are a part of the declaration list
            if( decl is FunctionDeclaration )
            {
               throw new ContextualAnalysisException( "Function Declarations cannot be a part of a declaration list" );
            }

            decl.Visit( this, null );
         }

         return null;
      }

      public object VisitDeclarationStatement( DeclarationStatement declarationStatement, object args )
      {
         declarationStatement.Declaration.Visit( this, null );
         return null;
     }

      public object VisitExpressions( Expressions expressions, object args )
      {
         foreach( var expr in expressions.List )
         {
            expr.Visit( this, null );
         }
         return null;
      }

      public object VisitExpressionStatement( ExpressionStatement expressionStatement, object args )
      {
         expressionStatement.Expression.Visit( this, null );
         return null;
      }

      public object VisitForStatement( ForStatement forStatement, object args )
      {
         m_IdTable.OpenScope();

         forStatement.InitializationExpression.Visit( this, null );
         forStatement.ConditionExpression.Visit( this, null );
         if( !forStatement.ConditionExpression.Info.TypeDenoter.Equals( BoolTypeDenoter.Normal ) )
         {
            throw new ContextualAnalysisException( "Condition expression of dejavu must be a pool" );
         }

         forStatement.IncrementationExpression.Visit( this, null );
         forStatement.Block.Visit( this, args );

         m_IdTable.CloseScope();

         return null;
      }

      public object VisitFunctionCallPrimaryExpression( FunctionCallPrimaryExpression functionCallPrimaryExpression, object args )
      {
         string id = (string) functionCallPrimaryExpression.Identifier.Visit( this, null );
         
         var decl = m_IdTable.Retrieve( id );

         if( decl == null )
         {
            throw new ContextualAnalysisException( "Usage of undeclared function: " + id );
         }
         else if( decl is FunctionDeclaration )
         {
            functionCallPrimaryExpression.Expressions.Visit( this, null );

            // This case is both for VariableDeclaration and VariableDeclarationWithDefinition
            functionCallPrimaryExpression.Declaration = (FunctionDeclaration) decl;

            for( int i = 0 ; i < functionCallPrimaryExpression.Declaration.Declarations.List.Count ; i++ )
            {
               var argDecl = functionCallPrimaryExpression.Declaration.Declarations.List[ i ];
               var argExpr = functionCallPrimaryExpression.Expressions.List[ i ];
               var argExprInfo = (ExpressionInfo) argExpr.Info;

               // make sure types match
               if( !argExprInfo.TypeDenoter.Equals( argDecl.Type ) )
               {
                  throw new ContextualAnalysisException( "Invalid argument type passed to function: " + id );
               }

               // we disallow doing the following: myFunction( "Hello" )?
               if( argExprInfo.ExpressionType == ExpressionType.Value && argExprInfo.TypeDenoter.Equals( CharTypeDenoter.Array ) )
               {
                  throw new ContextualAnalysisException( "Cannot pass in strings directly as arguments. They must first be wrapped in a char array" );
               }
            }

            functionCallPrimaryExpression.Info = new ExpressionInfo( ExpressionType.Value, decl.Type );
         }
         else
         {
            throw new ContextualAnalysisException( "Usage of invalid identifier: " + id );
         }
         return null;
      }

      public object VisitFunctionDeclaration( FunctionDeclaration functionDeclaration, object args )
      {
         string id = (string) functionDeclaration.Identifier.Visit( this, null );

         if( m_IdTable.ScopeLevel > 1 )
         {
            throw new ContextualAnalysisException( "Functions can only be declared in the top-most scope" );
         }

         m_IdTable.Enter( id, functionDeclaration );

         m_IdTable.OpenScope();

         functionDeclaration.Declarations.Visit( this, null );
         functionDeclaration.Block.Visit( this, functionDeclaration );
         functionDeclaration.Type.Visit( this, null );

         // check declarations
         for( int i = 0 ; i < functionDeclaration.Declarations.List.Count ; i++ )
         {
            var decl = functionDeclaration.Declarations.List[ i ];
            if( decl.Type.IsArray )
            {
               if( decl.Type.ArrayIndexer.HasIndex )
               {
                  throw new ContextualAnalysisException( "Cannot specifiy array index for declarations in function header: " + id );
               }
            }
         }

         m_IdTable.CloseScope();

         return null;
      }

      public object VisitIdentifier( Identifier identifier, object args )
      {
         return identifier.Spelling;
      }

      public object VisitIfStatement( IfStatement ifStatement, object args )
      {
         ifStatement.ConditionExpression.Visit( this, null );
         var info = ifStatement.ConditionExpression.Info;
         if( !info.TypeDenoter.Equals( BoolTypeDenoter.Normal ) )
         {
            throw new ContextualAnalysisException( "ConditionExpression of a when statement must be a pool" );
         }

         m_IdTable.OpenScope();
         ifStatement.IfBlock.Visit( this, args );
         m_IdTable.CloseScope();

         if( ifStatement.ElseBlock != null )
         {
            m_IdTable.OpenScope();
            ifStatement.ElseBlock.Visit( this, args );
            m_IdTable.CloseScope();
         }
         return null;
      }

      public object VisitIntegerTypeDenoter( IntegerTypeDenoter integerTypeDenoter, object args )
      {
         return null;
      }

      public object VisitIntegerTypeLiteral( IntegerTypeLiteral integerTypeLiteral, object args )
      {
         return IntegerTypeDenoter.Normal;
      }

      public object VisitOperator( Operator _operator, object args )
      {
         return null;
      }

      public object VisitOperatorModifiedExpressionPrimaryExpression( OperatorModifiedExpressionPrimaryExpression operatorModifiedExpressionPrimaryExpression, object args )
      {
         operatorModifiedExpressionPrimaryExpression.Operator.Visit( this, null );
         operatorModifiedExpressionPrimaryExpression.Expression.Visit( this, null );
         var type = operatorModifiedExpressionPrimaryExpression.Expression.Info;
         var modifiedType = new ExpressionInfo( ExpressionType.Value, type.TypeDenoter );
         operatorModifiedExpressionPrimaryExpression.Info = modifiedType;
         return null;
      }

      public object VisitParanthesisPrimaryExpression( ParanthesisPrimaryExpression paranthesisPrimaryExpression, object args )
      {
         paranthesisPrimaryExpression.Expression.Visit( this, null );
         var type = paranthesisPrimaryExpression.Expression.Info;
         var modifiedType = new ExpressionInfo( ExpressionType.Value, type.TypeDenoter );
         paranthesisPrimaryExpression.Info = modifiedType;
         return null;
      }

      public object VisitProgram( Program program, object args )
      {
         m_IdTable.OpenScope();

         bool foundEnterFunction = false;

         foreach( var decl in program.Declarations )
         {
            decl.Visit( this, null );

            if( decl is FunctionDeclaration && decl.Identifier.Spelling == "enter" )
            {
               foundEnterFunction = true;
            }
         }

         if( !foundEnterFunction )
         {
            throw new ContextualAnalysisException( "No enter function declared" );
         }

         m_IdTable.CloseScope();

         return null;
      }

      public object VisitReturnStatement( ReturnStatement returnStatement, object args )
      {
         returnStatement.Expression.Visit( this, null );
         var info = returnStatement.Expression.Info;

         // the argument is the function declaration that the statement is a part of
         var functionDecl = args as FunctionDeclaration;
         if( functionDecl == null || !functionDecl.Type.Equals( info.TypeDenoter ) )
         {
            throw new ContextualAnalysisException( "Invalid return type" );
         }

         return null;
      }

      public object VisitCharTypeDenoter( CharTypeDenoter stringTypeDenoter, object args )
      {
         return null;
      }

      public object VisitCharTypeLiteral( CharTypeLiteral stringTypeLiteral, object args )
      {
         return CharTypeDenoter.Normal;
      }

      public object VisitTypeLiteralPrimaryExpression( TypeLiteralPrimaryExpression typeLiteralPrimaryExpression, object args )
      {
         var denoter = (TypeDenoter) typeLiteralPrimaryExpression.TypeLiteral.Visit( this, null );
         typeLiteralPrimaryExpression.Info = new ExpressionInfo( ExpressionType.Value, denoter );
         return null;
      }

      public object VisitVariableDeclaration( VariableDeclaration variableDeclaration, object args )
      {
         string id = (string) variableDeclaration.Identifier.Visit( this, null );

         m_IdTable.Enter( id, variableDeclaration );

         if( variableDeclaration.Type.IsArray )
         {
            var indexer = variableDeclaration.Type.ArrayIndexer;
            if( indexer.HasIndex && !( indexer.IndexExpression is TypeLiteralPrimaryExpression ) )
            {
               throw new ContextualAnalysisException( "Cannot use expression to declare an array: " + id );
            }
         }

         variableDeclaration.Type.Visit( this, null );

         return null;
      }

      public object VisitVariablePrimaryExpression( VariablePrimaryExpression variablePrimaryExpression, object args )
      {
         string id = (string) variablePrimaryExpression.Identifier.Visit( this, null );

         var decl = m_IdTable.Retrieve( id );

         if( decl == null )
         {
            throw new ContextualAnalysisException( "Usage of undeclared variable: " + id );
         }
         else if( decl is VariableDeclaration )
         {
            // This case is both for VariableDeclaration and VariableDeclarationWithDefinition
            variablePrimaryExpression.Declaration = (VariableDeclaration) decl;
            variablePrimaryExpression.Info = new ExpressionInfo( ExpressionType.Variable, decl.Type );
            return null;
         }
         else
         {
            throw new ContextualAnalysisException( "Usage of invalid identifier: " + id );
         }
      }

      public object VisitVoidTypeDenoter( VoidTypeDenoter voidTypeDenoter, object args )
      {
         return null;
      }

      public object VisitWhileStatement( WhileStatement whileStatement, object args )
      {
         whileStatement.ConditionExpression.Visit( this, null );
         var info = whileStatement.ConditionExpression.Info;
         if( !info.TypeDenoter.Equals( BoolTypeDenoter.Normal ) )
         {
            throw new ContextualAnalysisException( "ConditionExpression of a aslongass statement must be a pool" );
         }
         m_IdTable.OpenScope();
         whileStatement.Block.Visit( this, args );
         m_IdTable.CloseScope();
         return null;
      }

      public object VisitOutputStatement( OutputStatement outputStatement, object args )
      {
         outputStatement.Expression.Visit( this, null );
         var expressionInfo = outputStatement.Expression.Info;
         var intType = expressionInfo.TypeDenoter as IntegerTypeDenoter;
         if( intType != null && intType.IsArray )
         {
            throw new ContextualAnalysisException( "Cannot output quantity array" );
         }

         var boolType = expressionInfo.TypeDenoter as BoolTypeDenoter;
         if( boolType != null && boolType.IsArray )
         {
            throw new ContextualAnalysisException( "Cannot output pool array" );
         }

         return null;
      }

      public object VisitInputStatement( InputStatement inputStatement, object args )
      {
         inputStatement.VariableExpression.Visit( this, null );

         var typeDenoter = inputStatement.VariableExpression.Info.TypeDenoter;
         if( !( inputStatement.VariableExpression is VariablePrimaryExpression ) || !( typeDenoter is CharTypeDenoter ) || !typeDenoter.IsArray )
         {
            throw new ContextualAnalysisException( "Must use input statement with an identifier of type char array" );
         }
         
         return null;
      }
   }
}
